---
title: Core actions
description: Building blocks of Tracecat workflows and action templates.
icon: cube
---

import SecretsExample from '/snippets/secrets-example.mdx'

<Tip>
  All core actions are open source and available in [Tracecat's GitHub repository](https://github.com/TracecatHQ/tracecat/tree/main/registry/tracecat_registry/core).
</Tip>

<Note>
  This tutorial covers the four most commonly used core actions: `core.transform.reshape`, `core.http_request`, `core.http_poll`, and `core.script.run_python`.

  To learn about the other core actions, check out the following tutorials:
  - [Script actions](/quickstart/script-actions)
  - [Data transforms](/tutorials/data-transforms)
  - [Scatter-gather](/tutorials/scatter-gather)
  - [Child workflows](/tutorials/child-workflows)
  - [Wait until](/tutorials/wait-retry-until)
</Note>

Core actions are the building blocks of Tracecat workflows and action templates.
All core actions are under the `core` namespace.
They are distinct from pre-built integrations in the `tools` namespace, which are pre-configured for specific 3rd-party tools.

There are five sub-namespaces under `core`:

- `core`
- `core.transform`
- `core.workflow`
- `core.require`
- `core.script`

`core` actions are the most commonly used actions in Tracecat.
They include:

| Name | Display Name | Description |
| --- | --- | --- |
| `core.transform.reshape` | `Reshape` | Reshape and manipulate data. |
| `core.transform.scatter` | `Scatter` | Split a list of data into individual items. |
| `core.transform.gather` | `Gather` | Merge scattered data back into a single list. |
| `core.http_request` | `HTTP Request` | Make a HTTP request. |
| `core.http_poll` | `HTTP Polling` | Poll a REST API until a condition is met. |
| `core.script.run_python` | `Run Python script` | Execute custom Python code in a secure sandbox. |

## Reshape

Reshape is a simple action that takes a single input `value` (e.g. a string, number, or object),
evaluates any [expressions and functions](/quickstart/expressions), and returns the result.

![Reshape](/img/quickstart/core-actions/reshape.png)

The following examples show common use-cases and inputs for the `Reshape` action.

<CodeGroup>
  ```php Basic usage
  # Get one value from a previous action
  value: ${{ ACTIONS.get_user.result.data.user.name }}

  # Get multiple values from a previous action
  value:
    name: ${{ ACTIONS.get_user.result.data.user.name }}
    email: ${{ ACTIONS.get_user.result.data.user.email }}

  # Get data from the trigger
  value: ${{ TRIGGER.data.user }}
  ```

  ```php Hardcode values
  # Hardcode a value
  value: "John Doe"

  ## Hardcode list of values
  value:
    - "John Doe"
    - "Jane Doe"

  ## Hardcode object
  value:
    name: "John Doe"
    email: "john.doe@example.com"
  ```

  ```php With inline functions
  # ACTIONS, TRIGGER, and FN are all available in the reshape action
  value:
    email: ${{ ACTIONS.get_user.result.data.user.name || "No email found" }}
    ip_version: ${{ FN.check_ip_version(ACTIONS.get_user.result.data.user.ip) }}
    iso_datetime: ${{ FN.to_isoformat(ACTIONS.get_user.result.data.user.created_at) }}
  ```

</CodeGroup>


<Tip>
  Use the `Reshape` action to:
  - Hardcode values
  - Rename data fields
  - Store data from previous actions or the trigger into a value or object
  - Transform data using [inline functions](/quickstart/expressions#fn-context)

  This action is one of the most commonly used and powerful action in Tracecat.
  Check out the [functions cheatsheet](/quickstart/functions) for a list of all the available functions.
</Tip>

## HTTP Request

Perform an HTTP request to a given URL.

**Parameters**

- `url` (HttpUrl, required): The destination of the HTTP request.
- `method` (Literal["GET", "POST", "PUT", "PATCH", "DELETE"], required): HTTP request method.
- `headers` (dict[str, str], optional): HTTP request headers.
- `params` (dict[str, Any], optional): URL query parameters.
- `payload` (dict[str, Any] | list[Any], optional): JSON serializable data in request body (POST, PUT, and PATCH).
- `form_data` (dict[str, Any], optional): Form encoded data in request body (POST, PUT, and PATCH).
- `files` (dict[str, str | FileUploadData], optional): Files to upload using multipart/form-data.
    - The dictionary key is the **form field name** for the file (e.g., `"file"`, `"attachment1"`).
    - The value can be:
        1.  A simple **base64 encoded string** representing the file content. In this case, the `form_field_name` will also be used as the filename in the `Content-Disposition` header.
        2.  A **dictionary (`FileUploadData`)** with the following keys:
            - `filename` (str): The actual filename to be sent in the `Content-Disposition` header (e.g., `"mydocument.pdf"`). If not provided or empty, the `form_field_name` will be used.
            - `content_base64` (str, required): The base64 encoded string of the file content.
            - `content_type` (str, optional): The MIME type of the file (e.g., `"application/pdf"`, `"image/png"`). If not provided, `httpx` will attempt to guess it.
- `auth` (dict[str, str], optional): Basic auth credentials with `username` and `password` keys.
- `timeout` (float, optional, default: 10.0): Timeout in seconds.
- `follow_redirects` (bool, optional, default: False): Follow HTTP redirects.
- `max_redirects` (int, optional, default: 20): Maximum number of redirects.
- `verify_ssl` (bool, optional, default: True): Verify SSL certificates.

**File Upload Examples (`files` parameter):**

1.  **Simple Base64 Upload (filename defaults to form field name):**

    ```yaml
    actions:
      - name: Upload Text File
        provider: core
        action: http_request
        params:
          url: https://httpbin.org/post
          method: POST
          files: {"my_text_file.txt": "{{ FN.to_base64(\"Hello, this is the file content!\") }}"}
    ```

2.  **Upload with Custom Filename and Content Type (using `FileUploadData` dict):**

    ```yaml
    actions:
      - name: Upload PDF Document
        provider: core
        action: http_request
        params:
          url: https://httpbin.org/post
          method: POST
          files: {
            "document_field":
              filename: "Annual Report.pdf"
              content_base64: "{{ FN.to_base64(ACTIONS.file_content.result.data) }}"
              content_type: "application/pdf"
          }
    ```

3.  **Multiple File Uploads (mixed simple and detailed):**

    ```yaml
    actions:
      - name: Upload Multiple Attachments
        provider: core
        action: http_request
        params:
          url: https://httpbin.org/post
          method: POST
          form_data: # Can be combined with files
            user_id: "user123"
          files: {
            "main_image.png": "{{ FN.to_base64(ACTIONS.file_content.result.data) }}"
            "additional_notes": # Detailed
              filename: "notes.txt"
              content_base64: "{{ FN.to_base64(\"Some notes here.\") }}"
              content_type: "text/plain"
          }
    ```

**Returns**

`HTTPResponse` (dict):

- `status_code` (int)
- `headers` (dict)
- `data` (str | dict | list | None)

## HTTP Polling

Perform an HTTP request to a given URL with polling.

**Parameters**

- Accepts all parameters from `core.http_request` **except for the `files` parameter.** `core.http_poll` does not support file uploads.
- `poll_retry_codes` (int | list[int], optional): Status codes on which the action will retry. If not specified, `poll_condition` must be provided.
- `poll_interval` (float, optional): Interval in seconds between polling attempts. If not specified, defaults to polling with exponential wait.
- `poll_max_attempts` (int, optional, default: 10): Maximum number of polling attempts. If set to 0, the action will poll indefinitely (until timeout).
- `poll_condition` (str, optional): User defined Python lambda function. When it returns a truthy value, stops polling. If not specified, `poll_retry_codes` must be provided.

**Returns**

`HTTPResponse` (dict) - same as `core.http_request`.

### Poll condition

The `poll_condition` parameter is a Python lambda function string that determines whether to retry.
The function receives a dict with `headers`, `data`, and `status_code` fields.
The function should return `True` if the polling should stop, and `False` if the polling should continue.

Examples:

<CodeGroup>
  ```python Poll until the `x-status` header is `completed`
  lambda x: x['headers'].get('x-status') == 'completed'
  ```

  ```python Poll until the `progress` field is `100`
  lambda x: x['data'].get('progress') == 100
  ```

  ```python Poll until the status code is `204`
  lambda x: x['status_code'] == 204
  ```
```
</CodeGroup>

## HTTP Paginate

Paginate through a HTTP response.

**Parameters**

- Accepts all parameters from `core.http_request` **except for the `files` parameter.** `core.http_paginate` does not support file uploads.
- `stop_condition` (str, required): User defined Python lambda function. When it returns a truthy value, stops pagination.
- `next_request` (str, required): User defined Python lambda function. Returns the next request as a JSON of `url`, `method`, `headers`, `params`, `payload`, `form_data` to paginate to.
- `items_jsonpath` (str, optional): JSONPath expression that evaluates to the items to paginate through.
- `limit` (int, optional, default: 1000): Maximum number of items to paginate through.

**Returns**

- With `items_jsonpath`: A flat list of items collected across pages, truncated to `limit` items.
- Without `items_jsonpath`: A list of per-page `HTTPResponse` objects, where `limit` applies per page.

Examples:

<CodeGroup>
  ```json Cursor in body (response example)
  {
    "items": [{"id": 1}, {"id": 2}],
    "cursor": "https://api.example.com/resources?cursor=abc123"
  }
  ```

  ```yaml Cursor in body (pagination config)
  stop_condition: "lambda x: x['data'].get('cursor') is None"
  next_request: "lambda x: {'url': x['data'].get('cursor')}"
  items_jsonpath: $.items
  limit: 1000
  ```

  ```json Offset + total (response example)
  {
    "items": [{"id": 101}, {"id": 102}],
    "offset": 0,
    "limit": 100,
    "total": 250
  }
  ```

  ```yaml Offset + total (pagination config)
  stop_condition: "lambda x: (x['data'].get('offset', 0) + x['data'].get('limit', 0)) >= x['data'].get('total', 0)"
  next_request: "lambda x: {'params': {'offset': x['data'].get('offset', 0) + x['data'].get('limit', 0)}}"
  items_jsonpath: $.items
  limit: 1000
  ```

  ```json nextPageToken in body (response example)
  {
    "items": [{"id": "a"}, {"id": "b"}],
    "nextPageToken": "token-123"
  }
  ```

  ```yaml nextPageToken in body (pagination config)
  stop_condition: "lambda x: not x['data'].get('nextPageToken')"
  next_request: "lambda x: {'params': {'pageToken': x['data'].get('nextPageToken')}}"
  items_jsonpath: $.items
  limit: 1000
  ```

  ```http Link header (response headers example)
  Link: <https://api.example.com/resources?page=3>; rel="next", <https://api.example.com/resources?page=50>; rel="last"
  ```

  ```yaml Link header (pagination config)
  # Stops when no rel="next" link is present
  stop_condition: "lambda x: 'link' not in {k.lower(): v for k, v in x['headers'].items()} or 'rel=\"next\"' not in {k.lower(): v for k, v in x['headers'].items()}['link']"
  # Picks the rel="next" URL from the Link header
  next_request: "lambda x: {'url': [p.split(';')[0].strip('<> ') for p in {k.lower(): v for k, v in x['headers'].items()}['link'].split(',') if 'rel=\"next\"' in p][0]}"
  items_jsonpath: $.items
  limit: 1000
  ```

  ```http Header token (response headers + body example)
  x-next-cursor: abc123
  ---
  { "items": [{"id": 1}], "count": 1 }
  ```

  ```yaml Header token (pagination config)
  stop_condition: "lambda x: not x['headers'].get('x-next-cursor')"
  next_request: "lambda x: {'params': {'cursor': x['headers'].get('x-next-cursor')}}"
  items_jsonpath: $.items
  limit: 1000
  ```

  ```json Stop on empty items (response example)
  { "items": [] }
  ```

  ```yaml Stop on empty items (pagination config)
  stop_condition: "lambda x: not x['data'].get('items')"
  next_request: "lambda x: {'params': {'page': (x['data'].get('page', 1) + 1)}}"
  items_jsonpath: $.items
  limit: 1000
  ```
</CodeGroup>

<Note>
  To capture the entire response body from each page as items, set `items_jsonpath` to `$`.
  This returns a flat list of page bodies (e.g., `[page1_body, page2_body, ...]`).
  If you omit `items_jsonpath`, the action returns a list of full `HTTPResponse` objects (headers, status_code, data) — one per page.
</Note>

## Tutorial: URLScan

URLScan uses a two-step process to get a threat intelligence report on a URL:

1. Call the `/scan` endpoint to submit the URL for scanning.
2. Poll the `/result` endpoint repeatedly until the status code changes from `404` to `200`.
3. Uses a reshape to extract the maliciousness score and categories from the response body.

<Steps>
  <Step title="Create URLScan secret">
    Add URLScan API key to Tracecat's built-in secrets manager.

    <SecretsExample />

  </Step>
  <Step title="Call /scan endpoint">
    Add the `core.http_request` action to your workflow.
    Rename it to `Submit URL` and configure it with the following inputs:

    ```yaml
    url: https://urlscan.io/api/v1/scan/
    method: POST
    headers:
      API-key: ${{ SECRETS.urlscan.URLSCAN_API_KEY }}
    payload:
      url: https://crowdstrikebluescreen.com
      visibility: private
    ```

    ![Scan URL](/img/quickstart/core-actions/scan-url.png)
  </Step>
  <Step title="Poll /result endpoint">
    Add the `core.http_poll` action to your workflow.
    Rename it to `Get result` and configure it with the following inputs:

    ```yaml
    url: https://urlscan.io/api/v1/result/${{ ACTIONS.scan_url.result.data.uuid }}
    method: GET
    poll_retry_codes: [404]
    headers:
      API-key: ${{ SECRETS.urlscan.URLSCAN_API_KEY }}
    ```

    ![Get URLScan result](/img/quickstart/core-actions/get-urlscan-result.png)
  </Step>
  <Step title="Get final verdict">
    Configure the reshape action to extract the maliciousness scores and categories from the response body.

    ```yaml
    value:
      verdict: ${{ ACTIONS.lookup_url.result.data.verdicts.urlscan.malicious }}
      score: ${{ ACTIONS.lookup_url.result.data.verdicts.urlscan.score }}
    ```

    ![View final verdict](/img/quickstart/core-actions/view-final-verdict.png)
  </Step>
  <Step title="Run workflow">
    Run the workflow to submit the URL for scanning and get the threat intelligence report.
    Under the hood, `Get result` calls the `/result` endpoint repeatedly until the status code is `200`.

    <Tabs>
      <Tab title="Events">
        ![View URLScan run](/img/quickstart/core-actions/view-urlscan-run.png)
      </Tab>
      <Tab title="Scan URL result">
        ![View scan URL result](/img/quickstart/core-actions/view-scan-url-result.png)
      </Tab>
      <Tab title="Get result">
        ![View urlscan result](/img/quickstart/core-actions/view-urlscan-result.png)
      </Tab>
    </Tabs>
  </Step>
</Steps>
